import org.junit.Test;

import java.io.*;
import java.util.*;

import static org.junit.Assert.*;

public class DocumentsProcessorTest {

    private final String testDirectory = "./test_files";
    private final String nGramFilePath = "./ngram_output.txt";
    private final DocumentsProcessor processor = new DocumentsProcessor();

    private void deleteFile() throws IOException {
        File dir = new File(testDirectory);
        if (!dir.exists()) {
            boolean created = dir.mkdirs();
            if (!created) {
                throw new IOException("Failed to create directory: " + dir.getAbsolutePath());
            }
        }

        if (dir.exists()) {
            for (File file : Objects.requireNonNull(dir.listFiles())) {
                boolean delete = file.delete();
                if (!delete) {
                    throw new IOException("Failed to delete file: " + file.getAbsolutePath());
                }
            }
        }
    }

    private void deleteFileForPart1() throws IOException {
        this.deleteFile();
        createTestFile("file1.txt", "This is a file.");
        createTestFile("file2.txt", "This is another file.");
        createTestFile("file3.txt", "Java is fun!");
    }


    // Part 1
    @Test
    public void testProcessDocuments() throws IOException {

        this.deleteFileForPart1();

        Map<String, List<String>> result = processor.processDocuments(testDirectory, 2);

        assertNotNull(result);
        assertEquals(3, result.size());

        assertTrue(result.containsKey("file1.txt"));
        assertTrue(result.containsKey("file2.txt"));
        assertTrue(result.containsKey("file3.txt"));

        assertEquals(Arrays.asList("thisis", "isa", "afile"), result.get("file1.txt"));
        assertEquals(Arrays.asList("thisis", "isanother", "anotherfile"), result.get("file2.txt"));
        assertEquals(Arrays.asList("javais", "isfun"), result.get("file3.txt"));
    }

    // Part 1
    @Test
    public void testEmptyFile() {
        String emptyContent = "";
        Reader reader = new StringReader(emptyContent);
        DocumentIterator iterator = new DocumentIterator(reader, 2);
        List<String> nGrams = new ArrayList<>();
        while (iterator.hasNext()) {
            nGrams.add(iterator.next());
        }
        assertTrue("Empty file should return empty list", nGrams.isEmpty());
    }

    // Part 1
    @Test
    public void testInsufficientWordsPartialNGram() {
        String content = "Hello";
        Reader reader = new StringReader(content);
        DocumentIterator iterator = new DocumentIterator(reader, 2);
        List<String> nGrams = new ArrayList<>();
        while (iterator.hasNext()) {
            nGrams.add(iterator.next());
        }
        List<String> expected = Collections.singletonList("hello");
        assertEquals("Error when not enough words for n-gram", expected, nGrams);
    }

    // Part 1
    @Test
    public void testInsufficientWordsPartialNGramMultiple() {
        String content = "Java is fun";
        Reader reader = new StringReader(content);
        DocumentIterator iterator = new DocumentIterator(reader, 4);
        List<String> nGrams = new ArrayList<>();
        while (iterator.hasNext()) {
            nGrams.add(iterator.next());
        }
        List<String> expected = Collections.singletonList("javaisfun");
        assertEquals("Error when not enough words for n-gram", expected, nGrams);
    }

    // Part 1
    @Test
    public void testNEqualsOne() {
        String content = "This is a test";
        Reader reader = new StringReader(content);
        DocumentIterator iterator = new DocumentIterator(reader, 1);
        List<String> nGrams = new ArrayList<>();
        while (iterator.hasNext()) {
            nGrams.add(iterator.next());
        }
        List<String> expected = Arrays.asList("this", "is", "a", "test");
        assertEquals(expected, nGrams);
    }

    private void deleteFileForPart2() throws IOException {
        this.deleteFile();
        createTestFile("file1.txt", "This is a test document.");
        createTestFile("file2.txt", "This is also a test document.");
    }

    // Part 2
    @Test
    public void testStoreNGrams() throws IOException {

        this.deleteFileForPart2();

        Map<String, List<String>> docs = new HashMap<>();
        docs.put("file1.txt", Arrays.asList("thisisatest", "isatestdocument"));
        docs.put("file2.txt", Arrays.asList("thisisalsoa", "isalsoatest", "alsoatestdocument"));

        List<Tuple<String, Integer>> result = processor.storeNGrams(docs, nGramFilePath);

        assertNotNull(result);
        assertEquals(2, result.size());

        Tuple<String, Integer> file1 = result.get(0);
        assertEquals("file1.txt", file1.getLeft());
        assertEquals(28, (int) file1.getRight());

        Tuple<String, Integer> file2 = result.get(1);
        assertEquals("file2.txt", file2.getLeft());
        assertEquals(42, (int) file2.getRight());

        File nGramFile = new File(nGramFilePath);
        assertTrue(nGramFile.exists());

        try (BufferedReader reader = new BufferedReader(new FileReader(nGramFilePath))) {
            String line1 = reader.readLine();
            assertNotNull(line1);
            assertEquals("thisisatest isatestdocument thisisalsoa isalsoatest alsoatestdocument",
                         line1.trim());
        }
    }

    private void deleteFileForPart3() throws IOException {
        this.deleteFile();
        createTestFile("test1.txt", "No one can copy from me. You can try if you want to");
        createTestFile("test2.txt", "One can copy from me. I will try to copy others.");
        createTestFile("test3.txt", "To copy others is my hobby. I can copy from anyone. You " +
                "want" + " to can copy from me as well?");
    }

    // Part 3
    @Test
    public void testComputeSimilarities() throws IOException {

        this.deleteFileForPart3();

        Map<String, List<String>> docs = processor.processDocuments(testDirectory, 3);
        List<Tuple<String, Integer>> fileIndex = processor.storeNGrams(docs, nGramFilePath);

        TreeSet<Similarities> similaritiesSet = processor.computeSimilarities(nGramFilePath,
                                                                              fileIndex);
        assertNotNull("Similarities set should not be null", similaritiesSet);
        assertEquals("Should have exactly 3 similarity pairs", 3, similaritiesSet.size());

        Iterator<Similarities> iterator = similaritiesSet.iterator();

        Similarities sim1 = iterator.next();
        assertEquals("First pair should have count of 3", 3, sim1.getCount());

        Similarities sim2 = iterator.next();
        assertEquals("Second pair should have count of 3", 3, sim2.getCount());

        Similarities sim3 = iterator.next();
        assertEquals("Third pair should have count of 3", 3, sim3.getCount());
    }

    // Part 4
    @Test
    public void testPrintSimilarities() {
        ByteArrayOutputStream outContent = new ByteArrayOutputStream();
        PrintStream originalOut = System.out;
        System.setOut(new PrintStream(outContent));

        Similarities sim1 = new Similarities("file1.txt", "file2.txt");
        sim1.setCount(5);
        Similarities sim2 = new Similarities("file1.txt", "file3.txt");
        sim2.setCount(3);
        Similarities sim3 = new Similarities("file2.txt", "file3.txt");
        sim3.setCount(7);
        TreeSet<Similarities> sims = new TreeSet<>(Arrays.asList(sim1, sim2, sim3));

        processor.printSimilarities(sims, 4);

        System.setOut(originalOut);

        String output = outContent.toString();
        String[] lines = output
                .trim()
                .split("\\n");

        assertEquals("Should have exactly 2 lines of output for threshold 4", 2, lines.length);
        assertTrue("First line should be the pair with highest count",
                   lines[0].contains("file2.txt") && lines[0].contains("file3.txt")
                           && lines[0].contains("7"));
        assertTrue("Second line should be the pair with second highest count",
                   lines[1].contains("file1.txt") && lines[1].contains("file2.txt")
                           && lines[1].contains("5"));
    }

    // Part 4
    @Test
    public void testPrintSimilaritiesWithHighThreshold() {

        ByteArrayOutputStream outContent = new ByteArrayOutputStream();
        PrintStream originalOut = System.out;
        System.setOut(new PrintStream(outContent));

        TreeSet<Similarities> sims = new TreeSet<>();
        Similarities sim1 = new Similarities("file1.txt", "file2.txt");
        sim1.setCount(5);
        sims.add(sim1);

        processor.printSimilarities(sims, 10);

        System.setOut(originalOut);

        String output = outContent
                .toString()
                .trim();
        assertEquals("Should have no output for threshold higher than all counts", "", output);
    }

    // Part 5
    @Test
    public void testProcessAndStore() throws IOException {
        this.deleteFileForPart2();

        List<Tuple<String, Integer>> result =
                processor.processAndStore(testDirectory, nGramFilePath, 4);

        assertNotNull("Result list should not be null", result);
        assertEquals("Should have exactly 2 files", 2, result.size());

        Tuple<String, Integer> file1 = result.get(0);
        assertEquals("file1.txt", file1.getLeft());
        assertEquals(28, (int) file1.getRight());

        Tuple<String, Integer> file2 = result.get(1);
        assertEquals("file2.txt", file2.getLeft());
        assertEquals(42, (int) file2.getRight());

        File nGramFile = new File(nGramFilePath);
        assertTrue("N-gram output file should exist", nGramFile.exists());

        try (BufferedReader reader = new BufferedReader(new FileReader(nGramFilePath))) {
            String line1 = reader.readLine();
            assertNotNull("First line should not be null", line1);
            assertEquals("thisisatest isatestdocument", line1.trim());

            String line2 = reader.readLine();
            assertNotNull("Second line should not be null", line2);
            assertEquals("thisisalsoa isalsoatest alsoatestdocument", line2.trim());
        }
    }



    private void createTestFile(String filename, String content) throws IOException {
        File dir = new File(testDirectory);
        if (!dir.exists()) {
            boolean created = dir.mkdirs();
            if (!created) {
                throw new IOException("Failed to create directory: " + dir.getAbsolutePath());
            }
        }

        File file = new File(dir, filename);
        try (FileWriter writer = new FileWriter(file)) {
            writer.write(content);
        }
    }

}
